package org.diy.traffic.config;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.logging.Level;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.diy.traffic.daemon.AirTrafficController;
import org.diy.traffic.daemon.AirTrafficController.ATCException;
import org.diy.traffic.sinks.TrafficSink;
import org.diy.traffic.sources.TrafficSource;
import org.diy.traffic.util.TLog;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


/**
* @author Shanks Krishnan, Dan Greenblatt
*DIYConfig is the class that encapsulates the diycity.config file. 
*This class has helper methods to retrieve all the city information in the config file as an ArrayList of DIYCity objects. 
*/


public class DataBotConfig {
    
    
	 /**
     * Sole constructor.  (For invocation by instantiating class 
     * typically implicit. Can be overridden)
     */

	private HashMap<String, DIYCity> registeredCities = new HashMap<String, DIYCity>();
	private String defaultConfigurationFile = null;
	private AirTrafficController atController;
	
    public DataBotConfig(AirTrafficController atc){
    	atController = atc;
    }
 
    public void registerCities(ArrayList<DIYCity> cities) {
    	Iterator<DIYCity> i = cities.iterator();
    	while (i.hasNext()) {
    		registerCity(i.next());
    	}		
    }
    
    public void registerCity(DIYCity c) {
    	registeredCities.put(c.getEncodedAddress(), c);
    }
    
    public void unregisterCity(String encodedAddress) {
    	registeredCities.remove(encodedAddress);
    }
    
    public void unregisterCity(String cityName, String stateCode) {
    	registeredCities.remove(
    			encodeAddress(new String[]{cityName, stateCode})
    			);
    	
    }
    
    public void clearRegisteredCities() {
    	registeredCities.clear();
    }
    
    public DIYCity isCityRegistered(String encodedCityName) {
    	return registeredCities.get(encodedCityName);
    }
    
    public DIYCity isCityRegistered(String cityName, String stateCode) {
    	return registeredCities.get(
    			encodeAddress(new String[]{cityName, stateCode})
    			);
    }
    
    public void setDefaultConfigurationFile(String filePath) {
    	this.defaultConfigurationFile = filePath;
    }
    
    public String getDefaultConfigurationFile() {
    	return this.defaultConfigurationFile;
    }
    
    public HashMap<String, String> configureFromRemoteFile(String remoteConfigURI) throws TrafficConfigException {
    	File f = this.getRemoteConfigFile(remoteConfigURI);
    	return configureFromLocalFile(f.getAbsolutePath());
    }
	
    public ArrayList<DIYCity> determineNewCities(ArrayList<DIYCity> newCities) {
    	
    	ArrayList<DIYCity> delta = new ArrayList<DIYCity>();
    	
    	Iterator<DIYCity> i = newCities.iterator();
    	while (i.hasNext()) {
    		DIYCity c = i.next();
    		if (!registeredCities.containsKey(c.getEncodedAddress())) {
    			delta.add(c);
    		}
    	}
    	
    	return delta;
    }
    
    /**
     * This method reads the Configuration File and encapsulates the information in the cityList ArrayList. This method is called 
     * by the constructor and should not be overridden unless the Configuration format changes
     *
     */
    public HashMap<String,String> configureFromLocalFile(String filePath) throws TrafficConfigException {
    	HashMap <String,String> botParams = new HashMap<String,String>();
    	
		//InputStreamReader configFile=new InputStreamReader(this.getClass().getResourceAsStream(configFileName));
    	File f = new File(filePath);
    	
    	InputStream configInputStream = null;
    	
    	
    	try {
    		//configFileStream = new InputStreamReader(new FileInputStream(f));
    		configInputStream = new FileInputStream(f);
    	} catch (FileNotFoundException fnfe) {
    		fnfe.printStackTrace();
    		TLog.log(Level.SEVERE, "Can't open config file " + f.getAbsolutePath());
    		throw new TrafficConfigException(fnfe.getMessage());
    	}
    	
    	Document response = null;
    	if (configInputStream != null)
			try {
				response = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(configInputStream);
			} catch (SAXException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			} catch (ParserConfigurationException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		else {
    		TLog.log(Level.SEVERE, "Can't open stream to config file");
    		throw new TrafficConfigException("Unable to open config file " + f.getAbsolutePath());
    	}
    	
    
    	XPathFactory factory = XPathFactory.newInstance();
        XPath xPath=factory.newXPath();
    	
    	NodeList botNodes;
        String botPath = "/traffic/bot";
        try {
        	// Get each <ResultSet><Result> node
        	botNodes = (NodeList)xPath.evaluate(botPath, response, XPathConstants.NODESET);
        } catch (XPathExpressionException xpee)  {
        	throw new TrafficConfigException(String.format("Error parsing node path \"%s\": %s", botPath, xpee.getMessage()));
        }
        
        Node botNode = botNodes.item(0);
        NodeList botConfigNodes = botNode.getChildNodes();
        for (int c=0; c<botConfigNodes.getLength(); c++) {
        	Node nextNode = botConfigNodes.item(c);
        	if (nextNode.getNodeType() == Node.ELEMENT_NODE) {
        		
        		String paramName = nextNode.getNodeName();
        		if (paramName.compareTo("pipe") == 0) continue;
        		
        		Node childNode = nextNode.getFirstChild(); 
        		
        		String paramValue = "";
        		if (childNode != null)  
        			paramValue = childNode.getNodeValue();
        		
        		TLog.log(Level.CONFIG, String.format("Found config info: %s -> %s", paramName, paramValue));
        		botParams.put(paramName, paramValue);
        	}
        }
        
        
    	NodeList nodes;
        String pipePath = "/traffic/bot/pipe";
        try {
        	// Get each <ResultSet><Result> node
        	nodes = (NodeList)xPath.evaluate(pipePath, response, XPathConstants.NODESET);
        } catch (XPathExpressionException xpee)  {
        	throw new TrafficConfigException(String.format("Error parsing node path \"%s\": %s", pipePath, xpee.getMessage()));
        }	
    	
        TLog.log(Level.CONFIG, String.format("Found %s pipes in config file '%s'", nodes.getLength(), f.getAbsolutePath()));
        
        for (int i=0; i<nodes.getLength(); i++)  {
        	// do something for each defined 'pipe'
        	Node pipe_node = nodes.item(i);
        	
        	NodeList source_nodes = null;
        	NodeList sink_nodes   = null;
        	
        	try {
				source_nodes = (NodeList)xPath.evaluate("source", pipe_node, XPathConstants.NODESET);
				sink_nodes   = (NodeList)xPath.evaluate("sink",   pipe_node, XPathConstants.NODESET);
			} catch (XPathExpressionException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
        	
			TLog.log(Level.CONFIG, String.format("Found %s sources in this pipe", source_nodes.getLength()));
			TLog.log(Level.CONFIG, String.format("Found %s sinks in this pipe", sink_nodes.getLength()));
			
			if ( (source_nodes ==null) || (sink_nodes==null) ) {
				//something bad happened!
			}
			

			ArrayList<TrafficSource> source_list = generateObjectsFromNodes(source_nodes, "TrafficSource");
			Iterator<TrafficSource> sourceI = source_list.iterator();
			while (sourceI.hasNext()) {
				TrafficSource tSource = sourceI.next();
				try {
					atController.registerTrafficSource(tSource);
				} catch (ATCException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
			
			ArrayList<TrafficSink>   sink_list   = generateObjectsFromNodes(sink_nodes, "TrafficSink");
			Iterator<TrafficSink> sinkI = sink_list.iterator();
			while (sinkI.hasNext()) {
				TrafficSink tSink= sinkI.next();
				try {
					atController.registerTrafficSink(tSink);
				} catch (ATCException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
			
			establishPipes(source_list, sink_list);
        	//for each source, establish a pipe with each sink
        }
		return botParams;
    }
        	
     
    private HashMap<String,String> getContentOfNode(Node a_node) {
    	HashMap<String, String> childrenContent = new HashMap<String, String>();
    	
    	NodeList childNodes = a_node.getChildNodes();
    	//System.err.println("there are " + childNodes.getLength() + " child nodes. they are:");
    	  	
    	for (Node child = a_node.getFirstChild(); 
        child != null;
        child = child.getNextSibling())
    	{
    		if (child.getNodeType() == child.TEXT_NODE){
    			//System.err.println("Text: "+child.getNodeValue());
    		} else if (child.getNodeType() == child.ELEMENT_NODE) {
    			
    			Node first_child = child.getFirstChild();
    			String node_value;
    			if (first_child == null) {
    				node_value="";
    			} else {
    				node_value = first_child.getNodeValue();
    			}
    			//System.err.println(child.getNodeName()+" = '" + node_value + "'");	
    			childrenContent.put(child.getNodeName(), node_value);
    		}
    	}
    	
    	/**
    	for (int i=0; i<childNodes.getLength(); i++) {
    		Node n = childNodes.item(i);
    		if (n.getNodeType() == Node.TEXT_NODE) {
    			childrenContent.put(n.getNodeName(), n.getNodeValue());
    		}
    	}
    	**/
    	
    	return childrenContent;
    }
    
    private ArrayList generateObjectsFromNodes(NodeList trafficSourceNodes, String srcOrSink) throws TrafficConfigException {
    		
    	ArrayList generated = null;
    	if (srcOrSink.compareTo("TrafficSource") == 0) {
    		generated = new ArrayList<TrafficSource>();
    	} else if (srcOrSink.compareTo("TrafficSink") == 0) {
    		generated = new ArrayList<TrafficSink>();
    	}

    	
    	for (int i=0; i<trafficSourceNodes.getLength(); i++) {
    		Node a_node = trafficSourceNodes.item(i);

    		NamedNodeMap attr_map = a_node.getAttributes();
    		    		
    		Node typeNode = attr_map.getNamedItem("type");
    		if (typeNode == null) {
    			throw new TrafficConfigException("Don't know what kind of source this represents, no 'type' specified.");
    		}
    		String node_name = typeNode.getNodeName();
    		String node_type = typeNode.getNodeValue();
    	
    		TLog.log(Level.CONFIG, String.format("Found a %s of type '%s'", srcOrSink, node_type));
    	
    		// try to get a reference to the class of which this node is a type.
    		Class node_class = null;
    		try {
				node_class = Class.forName(node_type);
			} catch (ClassNotFoundException e) {
				TLog.log(Level.SEVERE, String.format("Can't find class for name '%s'", node_type));
				continue;
			}
			
			Method factory_mthd = null;
			boolean use_node = false;
			try {
				try {
					factory_mthd = node_class.getMethod("createFromConfigNode", Node.class);
					TLog.log(Level.CONFIG, String.format("Configuring instant of class '%s' from Node", node_class));
					use_node = true;
				} catch (NoSuchMethodException nsme) {
					try {
						factory_mthd = node_class.getMethod("createFromConfigData", HashMap.class);
						TLog.log(Level.CONFIG, String.format("Configuring instant of class '%s' from data", node_class));
						use_node = false;
					} catch (NoSuchMethodException nsme2) {
						throw new TrafficConfigException("Unable to find appropriate factory class for " + node_type);
					}
				}
				
				HashMap<String,String> node_contents = null;
				if (!use_node) {
					node_contents = getContentOfNode(a_node);
					TLog.log(Level.CONFIG, "Found the following data elements in this config element:");
					printNodeContents(node_contents);
		    		//System.err.println("Node_contents here are " + node_contents.toString());
				}
						
				/*catch (SecurityException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IllegalArgumentException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} */
					
				/** not doing constructor anymore! **/
				//Constructor traffic_ctor = node_class.getConstructor(HashMap.class);
				
				
				if (srcOrSink.compareTo("TrafficSource") == 0) {
					TrafficSource t_source = null;
					if (use_node)
						t_source = (TrafficSource) factory_mthd.invoke(node_class, a_node);
					else 
						t_source = (TrafficSource) factory_mthd.invoke(node_class, node_contents);
					//TrafficSource t_source = (TrafficSource) traffic_ctor.newInstance(node_contents);
					if (t_source == null) {
						throw new TrafficConfigException("Error while building source " + node_class + " from config file");
					}
					
					generated.add(t_source);
				} else if (srcOrSink.compareTo("TrafficSink") == 0) {
					TrafficSink t_sink = null;
					if (use_node)
						t_sink = (TrafficSink) factory_mthd.invoke(node_class, a_node);
					else 
						t_sink = (TrafficSink) factory_mthd.invoke(node_class, node_contents);
 					
					if (t_sink== null) {
						throw new TrafficConfigException("Error while building sink " + node_class + " from config file");
					}
					
					//TrafficSink t_sink = (TrafficSink) traffic_ctor.newInstance(node_contents);
					generated.add(t_sink);
				}
			} catch (SecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
    	}
    	
    	return generated;
    }
      
    private void establishPipes(ArrayList<TrafficSource> sources, ArrayList<TrafficSink> sinks) {
    	Iterator<TrafficSource> sourceI = sources.iterator();
    	while (sourceI.hasNext()) {
    		TrafficSource tSource = sourceI.next();
    		
    		Iterator<TrafficSink> sinkI = sinks.iterator();
    		while (sinkI.hasNext()) {
    			TrafficSink tSink = sinkI.next();
    			
    			try {
					atController.establishPipe(tSource, tSink);
				} catch (ATCException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
    		}
    	}
    }
        /**
        String line = null;
        
        while (true) {
        	
        	try {
				line = bufferedReader.readLine();
			} catch (IOException e) {
				e.printStackTrace();
				throw new TrafficConfigException("Error occurred while reading line from config file");
			}
			
			//System.out.println("LINE IS " + line);
			
			if (line == null) break;
        	
			line = line.trim();
			
			if (line.isEmpty()) {
        		// just a blank line here
        		continue;
        	} else if ( line.substring(0, 2).compareTo("//") == 0) {
        		// if this line is a comment
        		continue;
        	}
        	
        	if (line.contains("<br>")) {
        		line = line.substring(0, line.indexOf("<br>"));
        	}
        	
        	String[] elements = line.split(",");
        	if (elements.length != 4) {
        		TLog.log(Level.WARNING, String.format("line '%s' should contain four elements, only contains %s. Ignoring.", line, elements.length));
        		continue;
        	}
        	
        	String city_name=elements[0].trim();
        	String state_code=elements[1].trim();
    		String twitter_username = elements[2].trim();	
    		String twitter_password = elements[3].trim();
    		
        	
    		TLog.log(Level.CONFIG, 
    				String.format("Found city info in the config file: %s, %s, %s, %s", city_name, state_code, twitter_username, twitter_password)
    		);
            		
    		cityList.add(
    				new DIYCity(city_name, state_code, twitter_username, twitter_password)
    				);                	
        }
	
        return cityList;
        **/  
    
    private File getRemoteConfigFile(String remoteFileURI) throws TrafficConfigException {
    	
    	
		final URLConnection connection;
		final InputStream inStream;
		try {
			connection = new URL(remoteFileURI).openConnection();
		} catch (IOException ex) {
			throw new TrafficConfigException("Unable to fetch remote config file");
		}
		
		/**
		// pretend to be IE6 on Windows XP SP2
		// http://en.wikipedia.org/wiki/User_agent#Internet_Explorer
		connection
				.setRequestProperty("User-Agent",
						"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)");
		connection.setDoInput(true);
		**/
		
		
		// ten second time-out
		connection.setReadTimeout(10 * 1000); 
			
		// Open a connection
		try {
			inStream = connection.getInputStream();
		} catch (FileNotFoundException e) { // This happens with 404s only
			throw new TrafficConfigException("Can't find remote config file (404 Error) on '" + remoteFileURI + "'");
		} catch (IOException e) { // All other problems
			TLog.log(Level.WARNING, "Unknown error while connecting to remote config file host");
			throw new TrafficConfigException(e.getMessage());
		}

		InputStreamReader reader = new InputStreamReader(inStream);
		BufferedReader bReader = new BufferedReader(reader);
		
		StringBuilder output = new StringBuilder();
		
		try {
			while (true) {
		
			int c = bReader.read();
			if (c == -1)
				break;
			output.append((char) c);
			}
		} catch (IOException ioe) {
			TLog.log(Level.WARNING, "Error downloading contents of remote config");
			throw new TrafficConfigException(ioe.getMessage());
		}
		
		String remote_file_contents = output.toString();
		File temp_file;
		try {
			temp_file = File.createTempFile("remoteconfig", "temp");
		} catch (IOException e) {
			TLog.log(Level.SEVERE, "Can't create a temp file");	
			throw new TrafficConfigException(e.getMessage());
		}
		
		BufferedWriter config_writer=null;
    	try {

    		String confFileName=temp_file.getAbsolutePath();
    		config_writer=new BufferedWriter(new FileWriter(confFileName));
			config_writer.write(remote_file_contents);
			config_writer.flush();			
		} catch (IOException e) {
			TLog.log(Level.WARNING, "Error writing to temp file " + temp_file.getAbsolutePath());	
			throw new TrafficConfigException(e.getMessage());
		}
		
		TLog.log(Level.INFO, "GOT REMOTE FILE CONTENTS!:\n " + remote_file_contents);
		return temp_file;
    }
       
    public String encodeAddress(String[] address) {
		String address_str = "";
		for (int i=0; i<address.length; i++){
			address_str += address[i].replace(" ", "-");
			if (i < address.length-1) address_str += "_";
		}
		return address_str;
	}
    
    public class TrafficConfigException extends Exception {

		public TrafficConfigException() {
			super();
		}

		public TrafficConfigException(String arg0) {
			super(arg0);
		}
    	
    }

    /** debugging function to print contents of a map **/
    private void printNodeContents (HashMap<String, String> aMap) {
    	Iterator i = aMap.keySet().iterator();
    	while (i.hasNext()) {
    		String key_string = (String) i.next();
    		TLog.log(Level.CONFIG, String.format("\"%s\" -> \"%s\"", key_string, aMap.get(key_string)));
    	}
    }
    
	public HashMap<String,String> configureFromDefaultFile() throws TrafficConfigException{
		if (this.defaultConfigurationFile != null) {
			return this.configureFromLocalFile(this.defaultConfigurationFile);
		} else
			return null;
	
	}
}
